Sanc 28 implement driver trip assignment system#56
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughRemoves Changes
Sequence DiagramsequenceDiagram
actor User
participant Frontend as BookingDebugPage
participant API as Bookings Router
participant DB as Database
participant Maps as GoogleMaps API
User->>Frontend: Request availability / submit booking
Frontend->>API: isDriverAvailable(driverId, startTime, endTime)
API->>DB: findOverlappingBooking(driverId, startTime, endTime)
DB-->>API: overlapping booking? / adjacent bookings
alt overlap detected
API-->>Frontend: {available:false, reason:"overlap"}
Frontend-->>User: Show overlap alert
else no overlap
API->>Maps: getTravelTimeMinutes(prev.pickup, new.pickup)
Maps-->>API: travel minutes (or null)
API->>Maps: getTravelTimeMinutes(new.destination, next.pickup)
Maps-->>API: travel minutes (or null)
API->>API: validateTravelBetweenAdjacentBookings(...)
alt travel validation fails
API-->>Frontend: {available:false, reason}
Frontend-->>User: Show travel-time alert
else validation succeeds
API-->>Frontend: {available:true}
Frontend-->>User: Enable submission
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In `@src/app/debug/bookings/page.tsx`:
- Around line 322-334: The onChange handler for DateInput uses
v.toISOString().slice(0,10) when v is a Date which converts to UTC and causes
off-by-one days in UTC+ timezones; update the branch handling v as a Date to
build the local date instead (use v.getFullYear(), v.getMonth(), v.getDate() or
construct new Date(v.getFullYear(), v.getMonth(), v.getDate())) and then call
setSelectedDay with that local-date value; keep the string branch parsing logic
for string inputs unchanged and ensure you still null-check v and call
setSelectedDay(null) when v is null (locations: the onChange handler, variable
v, and setSelectedDay).
In `@src/lib/datetime.ts`:
- Around line 3-14: Update the docstring in src/lib/datetime.ts for the rounding
function (the top comment block describing "Rounds a date up to the nearest time
slot increment") to correct the example showing 14:45 → 15:00; change it to
14:45 → 14:45 so the example accurately reflects that inputs already on the
increment boundary are unchanged (e.g., 14:07 → 14:15, 14:15 → 14:15, 14:16 →
14:30, 14:45 → 14:45).
In `@src/lib/google-maps.ts`:
- Around line 34-42: The JSDoc for the Google Maps travel-time function is
inaccurate: only the missing-API-key check actually throws, while HTTP/API
errors are caught and the function returns null; update the docblock for the
function (the "Calculates travel time between two addresses..." comment) to
remove or narrow the `@throws` description to only the missing API key case
(mention the specific missing-key check) and clarify in `@returns` that API
request failures result in a null return rather than an exception; reference the
function name/description in the file so you update the correct comment.
In `@src/server/api/routers/bookings.ts`:
- Around line 514-528: The current guard only calls validateDriverForSlot when
updatesToApply.driverId is present, so changes to startTime/endTime/addresses on
a booking with an existing driver skip re-validation; change the condition to
also run validation when there is an existing.driverId and any of
startTime/endTime/pickupAddress/destinationAddress are being changed. Inside
that block use driverId = updatesToApply.driverId ?? existing.driverId and
compute proposedStart/proposedEnd/proposedPickup/proposedDest the same way, then
await validateDriverForSlot(ctx, { driverId, startTime: proposedStart, endTime:
proposedEnd, pickupAddress: proposedPickup, destinationAddress: proposedDest,
excludeBookingId: id }); ensure the condition references
updatesToApply.startTime, updatesToApply.endTime, updatesToApply.pickupAddress,
and updatesToApply.destinationAddress and existing.driverId.
- Around line 222-234: listDrivers currently returns driver emails to any
authenticated caller; restrict access by checking the caller's role before
selecting the email column. In the listDrivers handler (protectedProcedure ->
listDrivers) either (A) enforce admin-only access by using your admin middleware
or checking ctx.session.user.role === "admin" and aborting with an unauthorized
error if not admin, or (B) keep the route public to authenticated users but
remove the email from the select for non-admins (select only id and name when
ctx.session.user.role !== "admin"). Update the select projection (the current
select of user.id, user.name, user.email) and the access check in listDrivers
accordingly so emails are only returned to admins.
🧹 Nitpick comments (5)
.gitignore (1)
12-13: Redundant ignore pattern.Since
drizzleon line 13 ignores the entire directory, thedrizzle/metaentry on line 12 is redundant and can be removed.♻️ Proposed cleanup
-drizzle/meta drizzlesrc/app/debug/bookings/page.tsx (3)
83-83:endPickerValuestate is set but never read.
endPickerValueis written on lines 184 and 225 but never consumed in the render output. The "Ends" field (line 442) reads fromestimatedEndQuery.data?.estimatedEndTimedirectly. Remove the unused state to avoid confusion.🧹 Remove unused state
- const [endPickerValue, setEndPickerValue] = useState<string | null>(null);And remove references on lines 184 and 225:
useEffect(() => { if (estimatedEndQuery.data) { - setEndPickerValue(estimatedEndQuery.data.estimatedEndTime); form.setFieldValue("end", estimatedEndQuery.data.estimatedEndTime); } }, [estimatedEndQuery.data, form.setFieldValue]);form.reset(); // ... reset selectedDay / startTimeOfDay ... - setEndPickerValue(null);
419-437: Remove commented-out code.Lines 423–426 contain commented-out JSX for location display. If this is intentionally omitted, remove it to keep the debug output clean.
364-407: Consider extracting the IIFE into a named component or variable.The inline IIFE
(() => { ... })()for rendering the driver bookings table works but reduces readability. Extracting it to a small helper or auseMemo-derived variable would improve clarity.src/server/api/routers/bookings.ts (1)
277-308:getEstimatedEndTimesilently uses a fallback when the API call fails — consider surfacing this.When
getTravelTimeMinutesreturnsnull, the fallback of 15 minutes is used without informing the client. If the actual travel time is significantly longer, the estimated end time could be too short, leading to booking conflicts. Consider returning a flag (e.g.,usedFallback: true) so the UI can display a warning.💡 Example: surface fallback usage
.query(async ({ input }) => { - const drivingTimeMinutes = - (await getTravelTimeMinutes(input.pickupAddress, input.destinationAddress)) ?? - FALLBACK_TRAVEL_MINUTES; + const apiResult = await getTravelTimeMinutes(input.pickupAddress, input.destinationAddress); + const usedFallback = apiResult === null; + const drivingTimeMinutes = apiResult ?? FALLBACK_TRAVEL_MINUTES; // ... existing calculation ... return { location1: input.pickupAddress, location2: input.destinationAddress, drivingTimeMinutes, totalBookingMinutes, startTime: input.startTime, estimatedEndTime, + usedFallback, }; }),
| /** | ||
| * Calculates travel time between two addresses using Google Maps Distance Matrix API | ||
| * | ||
| * @param originAddress - Origin address as a string | ||
| * @param destinationAddress - Destination address as a string | ||
| * @param options - Optional travel mode and unit settings | ||
| * @returns Travel duration in minutes, or null if calculation fails | ||
| * @throws Error if API key is missing or API request fails critically | ||
| */ |
There was a problem hiding this comment.
Docstring is misleading — HTTP errors do not propagate to callers.
The @throws annotation claims the function throws when "API request fails critically" (line 41), but the throw on line 72 is inside the try block and is caught by the catch on line 101, which returns null. Only the missing-API-key check (line 50–52) actually throws. Update the JSDoc to reflect reality.
📝 Suggested docstring fix
* `@returns` Travel duration in minutes, or null if calculation fails
- * `@throws` Error if API key is missing or API request fails critically
+ * `@throws` Error if GOOGLE_MAPS_API_KEY is not configured
*/📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** | |
| * Calculates travel time between two addresses using Google Maps Distance Matrix API | |
| * | |
| * @param originAddress - Origin address as a string | |
| * @param destinationAddress - Destination address as a string | |
| * @param options - Optional travel mode and unit settings | |
| * @returns Travel duration in minutes, or null if calculation fails | |
| * @throws Error if API key is missing or API request fails critically | |
| */ | |
| /** | |
| * Calculates travel time between two addresses using Google Maps Distance Matrix API | |
| * | |
| * `@param` originAddress - Origin address as a string | |
| * `@param` destinationAddress - Destination address as a string | |
| * `@param` options - Optional travel mode and unit settings | |
| * `@returns` Travel duration in minutes, or null if calculation fails | |
| * `@throws` Error if GOOGLE_MAPS_API_KEY is not configured | |
| */ |
🤖 Prompt for AI Agents
In `@src/lib/google-maps.ts` around lines 34 - 42, The JSDoc for the Google Maps
travel-time function is inaccurate: only the missing-API-key check actually
throws, while HTTP/API errors are caught and the function returns null; update
the docblock for the function (the "Calculates travel time between two
addresses..." comment) to remove or narrow the `@throws` description to only the
missing API key case (mention the specific missing-key check) and clarify in
`@returns` that API request failures result in a null return rather than an
exception; reference the function name/description in the file so you update the
correct comment.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
src/app/debug/bookings/page.tsx (1)
365-408: Consider extracting the IIFE-rendered driver schedule table to a named component.The immediately-invoked function expression inside JSX is functional but increases cognitive load. Extracting it to
<DriverScheduleTable bookings={...} day={...} />would improve readability and testability.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/debug/bookings/page.tsx` around lines 365 - 408, The JSX contains an IIFE that renders the driver schedule; extract it into a named component (e.g., DriverScheduleTable) to improve readability and testability: create a new React component DriverScheduleTable that accepts props bookings (from allBookingsQuery.data), day (selectedDay) and driverId (form.values.driverId), encapsulate the filtering (using bookingOverlapsDay), sorting, and rendering logic (including using formatTimeSlot and styles.bookedSlotsTableBody) inside that component, and replace the IIFE in page.tsx with <DriverScheduleTable bookings={allBookingsQuery.data ?? []} day={selectedDay} driverId={form.values.driverId} />.src/server/db/booking-schema.ts (1)
43-51: Consider a supplementary index to supportvalidateTravelBetweenAdjacentBookings.
bookings_driver_overlap_idxon(driverId, status, startTime, endTime)fitsfindOverlappingBookingwell (leadingdriverIdequality +startTimerange). However, the adjacent-booking queries invalidateTravelBetweenAdjacentBookingsfilter onendTime ≤ x(prev-booking) andstartTime ≥ x(next-booking) with ane(status, "cancelled")predicate. Becauseneon the second column (status) breaks the index prefix continuity,endTime(4th column) is effectively unreachable for the prev-booking lookup.A supplementary partial index (or a separate index on
(driverId, endTime)) would better serve the prev-booking query:-- PostgreSQL partial index (Drizzle doesn't expose .where() on index yet; apply as a raw migration) CREATE INDEX bookings_driver_end_time_idx ON bookings (driver_id, end_time) WHERE status <> 'cancelled';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/server/db/booking-schema.ts` around lines 43 - 51, The current composite index bookings_driver_overlap_idx (driverId, status, startTime, endTime) doesn't support the prev/next lookups used by validateTravelBetweenAdjacentBookings because the ne(status, "cancelled") predicate breaks the index prefix; add a supplementary index optimized for the prev-booking query—e.g., create a new index named bookings_driver_end_time_idx on (driver_id, end_time) that is either a partial index with WHERE status <> 'cancelled' or a standalone (driverId, endTime) index; since Drizzle doesn't expose .where() on indexes yet, implement the partial index as a raw SQL migration (or add the plain (driverId, endTime) index) so validateTravelBetweenAdjacentBookings can efficiently find the previous booking by endTime.src/server/api/routers/bookings.ts (1)
288-292: External API call inside a tRPCquery— cache/rate-limit in production.
getEstimatedEndTimecallsgetTravelTimeMinutes(Google Maps) on every execution. As aqueryendpoint it can be called by React Query on every mount/focus. The debug page setsstaleTime: 0, which causes frequent refetches. Before exposing this to production UI, add a reasonablestaleTimeon the client and consider server-side result caching to reduce API quota consumption.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/server/api/routers/bookings.ts` around lines 288 - 292, The query endpoint that computes drivingTimeMinutes calls getTravelTimeMinutes (via getEstimatedEndTime) on every request which can exhaust Google Maps quota; update the .query handler (where drivingTimeMinutes is computed) to: 1) add a server-side cache keyed by the input pair (pickupAddress + destinationAddress) with a sensible TTL (e.g., 5–15 minutes) using your app cache (in-memory or Redis) and return the cached travel time when present; 2) fall back to FALLBACK_TRAVEL_MINUTES only if both cache miss and API call fail; and 3) keep the API call guarded with try/catch to avoid throwing on external API errors. Also ensure the client that calls this tRPC query sets a non‑zero staleTime (e.g., several minutes) in React Query to avoid refetch storms.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/app/debug/bookings/page.tsx`:
- Around line 53-65: The function bookingOverlapsDay mixes local-time day
boundaries with UTC booking timestamps; update bookingOverlapsDay to compute
dayStart/dayEnd in UTC (e.g., construct dayStart via
Date.UTC(day.getUTCFullYear(), day.getUTCMonth(), day.getUTCDate()) or call
setUTCHours(0,0,0,0) and dayEnd via setUTCHours(23,59,59,999)) so that the day
boundaries and the parsed booking times (new Date(booking.startTime)/endTime)
are in the same UTC reference frame before comparing start/end/getTime.
In `@src/server/api/routers/bookings.ts`:
- Around line 362-370: There is a TOCTOU race because validateDriverForSlot
(read) and the subsequent ctx.db.insert (write) in the create mutation — and
similarly the update mutation that calls validateDriverForSlot — are separate
operations; wrap the read+write in a single transaction with a stricter
isolation level (e.g., START TRANSACTION; SET TRANSACTION ISOLATION LEVEL
REPEATABLE READ) so validateDriverForSlot runs inside the same transaction
context before calling ctx.db.insert/ctx.db.update, then commit; alternatively
acquire a PostgreSQL advisory lock keyed by driverId or add an exclusion
constraint on the tsrange to prevent overlapping bookings at the DB level and
remove the race. Ensure the fixes are applied to the create handler that calls
validateDriverForSlot and the update handler that calls the same function
(validateDriverForSlot and the corresponding ctx.db.update).
- Around line 127-130: When getTravelTimeMinutes(prevBooking.destinationAddress,
pickupAddress) returns null you silently use FALLBACK_TRAVEL_MINUTES; change
this to log a warning whenever a fallback is used so API failures
(network/quota/invalid address) are visible: after calling getTravelTimeMinutes,
if the result is null, call the existing logger (e.g., logger or processLogger)
to warn that travel-time lookup failed, include the from/to addresses
(prevBooking.destinationAddress and pickupAddress) and that
FALLBACK_TRAVEL_MINUTES is being used, then proceed to compute requiredMinutes =
FALLBACK_TRAVEL_MINUTES + TRAVEL_BUFFER_MINUTES; apply the identical
logging/fallback pattern to the next-booking travel check as well.
---
Duplicate comments:
In `@src/server/api/routers/bookings.ts`:
- Around line 222-235: The duplicate review comment indicates no code change is
needed because listDrivers already conditionally includes email for admins via
the isAdmin check in the protectedProcedure query; resolve this by removing or
marking the duplicate review comment as resolved in the PR (no code edits to
ctx.db/select or the isAdmin conditional in listDrivers are necessary).
- Around line 515-524: Remove the leftover duplicate review marker and clarify
the intent around driver revalidation: delete the stray "[duplicate_comment]"
and, if needed, add a short inline comment near the driverId /
needsDriverValidation logic (variables: driverId, needsDriverValidation,
updatesToApply, existing) explaining that driver revalidation is intentionally
triggered when driverId is present and any of driver/time/address fields change;
keep the boolean condition as-is.
---
Nitpick comments:
In `@src/app/debug/bookings/page.tsx`:
- Around line 365-408: The JSX contains an IIFE that renders the driver
schedule; extract it into a named component (e.g., DriverScheduleTable) to
improve readability and testability: create a new React component
DriverScheduleTable that accepts props bookings (from allBookingsQuery.data),
day (selectedDay) and driverId (form.values.driverId), encapsulate the filtering
(using bookingOverlapsDay), sorting, and rendering logic (including using
formatTimeSlot and styles.bookedSlotsTableBody) inside that component, and
replace the IIFE in page.tsx with <DriverScheduleTable
bookings={allBookingsQuery.data ?? []} day={selectedDay}
driverId={form.values.driverId} />.
In `@src/server/api/routers/bookings.ts`:
- Around line 288-292: The query endpoint that computes drivingTimeMinutes calls
getTravelTimeMinutes (via getEstimatedEndTime) on every request which can
exhaust Google Maps quota; update the .query handler (where drivingTimeMinutes
is computed) to: 1) add a server-side cache keyed by the input pair
(pickupAddress + destinationAddress) with a sensible TTL (e.g., 5–15 minutes)
using your app cache (in-memory or Redis) and return the cached travel time when
present; 2) fall back to FALLBACK_TRAVEL_MINUTES only if both cache miss and API
call fail; and 3) keep the API call guarded with try/catch to avoid throwing on
external API errors. Also ensure the client that calls this tRPC query sets a
non‑zero staleTime (e.g., several minutes) in React Query to avoid refetch
storms.
In `@src/server/db/booking-schema.ts`:
- Around line 43-51: The current composite index bookings_driver_overlap_idx
(driverId, status, startTime, endTime) doesn't support the prev/next lookups
used by validateTravelBetweenAdjacentBookings because the ne(status,
"cancelled") predicate breaks the index prefix; add a supplementary index
optimized for the prev-booking query—e.g., create a new index named
bookings_driver_end_time_idx on (driver_id, end_time) that is either a partial
index with WHERE status <> 'cancelled' or a standalone (driverId, endTime)
index; since Drizzle doesn't expose .where() on indexes yet, implement the
partial index as a raw SQL migration (or add the plain (driverId, endTime)
index) so validateTravelBetweenAdjacentBookings can efficiently find the
previous booking by endTime.
…ansaction for driver validation
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
src/server/db/booking-schema.ts (1)
45-50:bookings_driver_overlap_idxcovers the common overlap query pattern, but a secondary range predicate onendTimewill be a residual filter.A standard PostgreSQL B-tree index on
(driverId, status, startTime, endTime)efficiently handles the equality filter ondriverId/statusand a range scan onstartTime, but the conditionendTime > :bookingStartcannot be range-scanned simultaneously — it remains a row-level filter after the index scan. For the expected booking volumes in this application this is acceptable, but be aware that query planning may scan more index pages than a true range index would.The separately added
bookings_driver_end_time_idxon(driverId, endTime)is a good complement and not redundant: sincestatussits betweendriverIdandendTimein the overlap index, PostgreSQL cannot use the overlap index for queries that filter only on(driverId, endTime)without astatuspredicate.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/server/db/booking-schema.ts` around lines 45 - 50, The overlap index bookings_driver_overlap_idx on (driverId, status, startTime, endTime) cannot fully satisfy queries that need a separate range on endTime (endTime > :bookingStart) because PostgreSQL can only range-scan one trailing column, so endTime becomes a residual filter; keep the complementary bookings_driver_end_time_idx on (driverId, endTime) and add a brief inline comment next to the index definitions (referencing bookings_driver_overlap_idx and bookings_driver_end_time_idx and table.driverId/table.status/table.startTime/table.endTime) explaining this limitation and why the second index is required, or if you prefer to optimize different query shapes consider adding/ordering an index specifically for the common query (e.g., (driverId, endTime, startTime)) after profiling.src/app/debug/bookings/page.tsx (1)
240-255: Consider adding astaleTimetoavailabilityQueryto avoid hammering the Google Maps API on every keystroke.Once
canCheckAvailabilityistrue, any edit topickupAddressordestinationAddresschanges the query key and fires a new request immediately (defaultstaleTime: 0). Per the AI summary,isDriverAvailablemay delegate togetTravelTimeMinutesinsrc/lib/google-maps.ts. A short cache window avoids redundant external calls while the user is still typing.♻️ Suggested addition
{ enabled: canCheckAvailability }, + { enabled: canCheckAvailability, staleTime: 30 * 1000 },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/debug/bookings/page.tsx` around lines 240 - 255, The query for driver availability (availabilityQuery using api.bookings.isDriverAvailable.useQuery) fires on every change because staleTime defaults to 0; add a small staleTime (e.g., 1000–2000 ms) to the query options so edits to pickupAddress/destinationAddress don't immediately refetch while the user is typing, keeping canCheckAvailability logic unchanged and preserving caching for calls that may delegate to getTravelTimeMinutes.src/server/api/routers/bookings.ts (1)
29-29:travelTimeCachenever evicts expired entries.Expired entries are checked on read but never removed; in a long-running Node process they accumulate indefinitely. A simple prune-on-miss keeps memory bounded.
♻️ Minimal eviction on read
const cached = travelTimeCache.get(cacheKey); let drivingTimeMinutes: number; if (cached && cached.expiresAt > now) { drivingTimeMinutes = cached.value; } else { + travelTimeCache.delete(cacheKey); // prune expired entry const apiResult = await getTravelTimeMinutes(input.pickupAddress, input.destinationAddress);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/server/api/routers/bookings.ts` at line 29, travelTimeCache is never removing expired entries which causes unbounded memory growth; modify the access logic that reads from travelTimeCache (the Map named travelTimeCache) to prune expired entries when they are encountered (e.g., on a cache miss or when checking an entry's expiresAt) by deleting Map keys whose expiresAt <= Date.now(), and optionally perform a small bounded sweep (e.g., iterate a few keys) on misses to remove other expired items so the Map stays bounded during long-running processes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/server/api/routers/bookings.ts`:
- Around line 524-526: The update input schema for the bookings router is
missing the same endTime > startTime validation present in the create schema;
update the Zod schema used by the update resolver (the object containing
startTime: z.string().datetime().optional(), endTime:
z.string().datetime().optional()) to add a .refine() that enforces when both
startTime and endTime are provided that endTime > startTime (same logic as in
the create schema), so that findOverlappingBooking and row integrity are
preserved.
- Around line 319-325: The current logic stores FALLBACK_TRAVEL_MINUTES into
travelTimeCache when getTravelTimeMinutes returns null, which causes silent
serving of the fallback for TRAVEL_TIME_CACHE_TTL_MS; change the behavior in the
block handling getTravelTimeMinutes so that if apiResult is null you set
drivingTimeMinutes = FALLBACK_TRAVEL_MINUTES but do NOT write to travelTimeCache
(skip the cache write) and log a warning/notice similar to
validateTravelBetweenAdjacentBookings to surface the failure; keep cache writes
only when apiResult is a non-null number and continue to use the same cache key
and TTL constants (travelTimeCache, FALLBACK_TRAVEL_MINUTES,
TRAVEL_TIME_CACHE_TTL_MS, getTravelTimeMinutes,
validateTravelBetweenAdjacentBookings).
- Around line 35-47: assertCanAccessBooking currently only allows admins or
agency owners, which blocks assigned drivers; change its signature to accept an
optional bookingDriverId (e.g., assertCanAccessBooking(session, agencyId,
bookingDriverId?)) and treat the caller as allowed if role === "admin" ||
agencyId === session.user.id || bookingDriverId === session.user.id. Then update
getById to fetch the booking first and call assertCanAccessBooking(session,
booking.agencyId, booking.driverId) before returning; leave update and cancel
calls using the original two-argument semantics if drivers should remain unable
to mutate bookings.
---
Duplicate comments:
In `@src/app/debug/bookings/page.tsx`:
- Around line 53-64: The function bookingOverlapsDay builds UTC-day boundaries
using UTC getters from the provided local-midnight Date, which misaligns for
UTC+ zones; update bookingOverlapsDay to use local getters (getFullYear,
getMonth, getDate) when computing dStart/dEnd so the UTC boundaries match how
selectedDay/computedStart are constructed and keep the overlap check (start <
dEnd && end > dStart) unchanged; look for bookingOverlapsDay and ensure it
mirrors the same "local getters -> UTC boundary" approach as computedStart to
fix the off-by-one in UTC+ timezones.
---
Nitpick comments:
In `@src/app/debug/bookings/page.tsx`:
- Around line 240-255: The query for driver availability (availabilityQuery
using api.bookings.isDriverAvailable.useQuery) fires on every change because
staleTime defaults to 0; add a small staleTime (e.g., 1000–2000 ms) to the query
options so edits to pickupAddress/destinationAddress don't immediately refetch
while the user is typing, keeping canCheckAvailability logic unchanged and
preserving caching for calls that may delegate to getTravelTimeMinutes.
In `@src/server/api/routers/bookings.ts`:
- Line 29: travelTimeCache is never removing expired entries which causes
unbounded memory growth; modify the access logic that reads from travelTimeCache
(the Map named travelTimeCache) to prune expired entries when they are
encountered (e.g., on a cache miss or when checking an entry's expiresAt) by
deleting Map keys whose expiresAt <= Date.now(), and optionally perform a small
bounded sweep (e.g., iterate a few keys) on misses to remove other expired items
so the Map stays bounded during long-running processes.
In `@src/server/db/booking-schema.ts`:
- Around line 45-50: The overlap index bookings_driver_overlap_idx on (driverId,
status, startTime, endTime) cannot fully satisfy queries that need a separate
range on endTime (endTime > :bookingStart) because PostgreSQL can only
range-scan one trailing column, so endTime becomes a residual filter; keep the
complementary bookings_driver_end_time_idx on (driverId, endTime) and add a
brief inline comment next to the index definitions (referencing
bookings_driver_overlap_idx and bookings_driver_end_time_idx and
table.driverId/table.status/table.startTime/table.endTime) explaining this
limitation and why the second index is required, or if you prefer to optimize
different query shapes consider adding/ordering an index specifically for the
common query (e.g., (driverId, endTime, startTime)) after profiling.
| function assertCanAccessBooking( | ||
| session: { user: { id: string; role?: string | null } }, | ||
| agencyId: string, | ||
| ): void { | ||
| const role = session.user.role ?? "user"; | ||
| const allowed = role === "admin" || agencyId === session.user.id; | ||
| if (!allowed) { | ||
| throw new TRPCError({ | ||
| code: "FORBIDDEN", | ||
| message: "You don't have permission to access this booking.", | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
Drivers are blocked from getById for bookings they are assigned to.
assertCanAccessBooking only passes when the caller is an admin or when agencyId === session.user.id (agency-role users). However, getAll explicitly includes eq(bookings.driverId, userId) in its filter, so assigned drivers can list a booking through getAll but will receive FORBIDDEN when calling getById, update, or cancel on that same booking ID.
For getById at minimum, the check should also allow the assigned driver:
🐛 Proposed fix
function assertCanAccessBooking(
session: { user: { id: string; role?: string | null } },
agencyId: string,
+ driverId?: string | null,
): void {
const role = session.user.role ?? "user";
- const allowed = role === "admin" || agencyId === session.user.id;
+ const allowed =
+ role === "admin" ||
+ agencyId === session.user.id ||
+ (!!driverId && driverId === session.user.id);
if (!allowed) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You don't have permission to access this booking.",
});
}
}Then update the getById call:
- assertCanAccessBooking(ctx.session, row.agencyId);
+ assertCanAccessBooking(ctx.session, row.agencyId, row.driverId);Keep update and cancel using the stricter two-argument form if drivers should not be able to mutate bookings.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function assertCanAccessBooking( | |
| session: { user: { id: string; role?: string | null } }, | |
| agencyId: string, | |
| ): void { | |
| const role = session.user.role ?? "user"; | |
| const allowed = role === "admin" || agencyId === session.user.id; | |
| if (!allowed) { | |
| throw new TRPCError({ | |
| code: "FORBIDDEN", | |
| message: "You don't have permission to access this booking.", | |
| }); | |
| } | |
| } | |
| function assertCanAccessBooking( | |
| session: { user: { id: string; role?: string | null } }, | |
| agencyId: string, | |
| driverId?: string | null, | |
| ): void { | |
| const role = session.user.role ?? "user"; | |
| const allowed = | |
| role === "admin" || | |
| agencyId === session.user.id || | |
| (!!driverId && driverId === session.user.id); | |
| if (!allowed) { | |
| throw new TRPCError({ | |
| code: "FORBIDDEN", | |
| message: "You don't have permission to access this booking.", | |
| }); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/server/api/routers/bookings.ts` around lines 35 - 47,
assertCanAccessBooking currently only allows admins or agency owners, which
blocks assigned drivers; change its signature to accept an optional
bookingDriverId (e.g., assertCanAccessBooking(session, agencyId,
bookingDriverId?)) and treat the caller as allowed if role === "admin" ||
agencyId === session.user.id || bookingDriverId === session.user.id. Then update
getById to fetch the booking first and call assertCanAccessBooking(session,
booking.agencyId, booking.driverId) before returning; leave update and cancel
calls using the original two-argument semantics if drivers should remain unable
to mutate bookings.
| const apiResult = await getTravelTimeMinutes(input.pickupAddress, input.destinationAddress); | ||
| drivingTimeMinutes = apiResult ?? FALLBACK_TRAVEL_MINUTES; | ||
| travelTimeCache.set(cacheKey, { | ||
| value: drivingTimeMinutes, | ||
| expiresAt: now + TRAVEL_TIME_CACHE_TTL_MS, | ||
| }); | ||
| } |
There was a problem hiding this comment.
API failure fallback is cached, suppressing retries for 10 minutes.
When getTravelTimeMinutes returns null (quota, network, invalid address), drivingTimeMinutes is set to FALLBACK_TRAVEL_MINUTES and that value is written into the cache. Every subsequent call for the same address pair within the TTL window will silently serve the stale fallback without retrying the real API. No warning is logged here either, unlike the equivalent guard in validateTravelBetweenAdjacentBookings.
🐛 Proposed fix — skip cache write on failure
- const apiResult = await getTravelTimeMinutes(input.pickupAddress, input.destinationAddress);
- drivingTimeMinutes = apiResult ?? FALLBACK_TRAVEL_MINUTES;
- travelTimeCache.set(cacheKey, {
- value: drivingTimeMinutes,
- expiresAt: now + TRAVEL_TIME_CACHE_TTL_MS,
- });
+ const apiResult = await getTravelTimeMinutes(input.pickupAddress, input.destinationAddress);
+ if (apiResult === null) {
+ console.warn("[getEstimatedEndTime] Travel time lookup failed; using fallback", {
+ from: input.pickupAddress,
+ to: input.destinationAddress,
+ fallback: FALLBACK_TRAVEL_MINUTES,
+ });
+ }
+ drivingTimeMinutes = apiResult ?? FALLBACK_TRAVEL_MINUTES;
+ if (apiResult !== null) {
+ travelTimeCache.set(cacheKey, {
+ value: drivingTimeMinutes,
+ expiresAt: now + TRAVEL_TIME_CACHE_TTL_MS,
+ });
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/server/api/routers/bookings.ts` around lines 319 - 325, The current logic
stores FALLBACK_TRAVEL_MINUTES into travelTimeCache when getTravelTimeMinutes
returns null, which causes silent serving of the fallback for
TRAVEL_TIME_CACHE_TTL_MS; change the behavior in the block handling
getTravelTimeMinutes so that if apiResult is null you set drivingTimeMinutes =
FALLBACK_TRAVEL_MINUTES but do NOT write to travelTimeCache (skip the cache
write) and log a warning/notice similar to validateTravelBetweenAdjacentBookings
to surface the failure; keep cache writes only when apiResult is a non-null
number and continue to use the same cache key and TTL constants
(travelTimeCache, FALLBACK_TRAVEL_MINUTES, TRAVEL_TIME_CACHE_TTL_MS,
getTravelTimeMinutes, validateTravelBetweenAdjacentBookings).
| startTime: z.string().datetime().optional(), | ||
| endTime: z.string().datetime().optional(), | ||
| }), |
There was a problem hiding this comment.
update input lacks endTime > startTime validation, unlike create.
create enforces endTime > startTime via .refine(). update doesn't, so a client can pass an inverted range and corrupt the row. An inverted [startTime, endTime] also makes the overlap query in findOverlappingBooking behave unexpectedly (any booking can satisfy lt(startTime, endTime[past]) AND gt(endTime, startTime[future])).
🐛 Proposed fix
z.object({
id: z.number(),
title: z.string().optional(),
pickupAddress: z.string().optional(),
destinationAddress: z.string().optional(),
purpose: z.string().optional(),
passengerInfo: z.string().optional(),
driverId: z.string().optional().nullable(),
status: StatusZ.optional(),
startTime: z.string().datetime().optional(),
endTime: z.string().datetime().optional(),
- }),
+ }).refine(
+ (data) =>
+ data.startTime === undefined ||
+ data.endTime === undefined ||
+ new Date(data.endTime) > new Date(data.startTime),
+ { message: "endTime must be after startTime", path: ["endTime"] },
+ ),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| startTime: z.string().datetime().optional(), | |
| endTime: z.string().datetime().optional(), | |
| }), | |
| z.object({ | |
| id: z.number(), | |
| title: z.string().optional(), | |
| pickupAddress: z.string().optional(), | |
| destinationAddress: z.string().optional(), | |
| purpose: z.string().optional(), | |
| passengerInfo: z.string().optional(), | |
| driverId: z.string().optional().nullable(), | |
| status: StatusZ.optional(), | |
| startTime: z.string().datetime().optional(), | |
| endTime: z.string().datetime().optional(), | |
| }).refine( | |
| (data) => | |
| data.startTime === undefined || | |
| data.endTime === undefined || | |
| new Date(data.endTime) > new Date(data.startTime), | |
| { message: "endTime must be after startTime", path: ["endTime"] }, | |
| ), |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/server/api/routers/bookings.ts` around lines 524 - 526, The update input
schema for the bookings router is missing the same endTime > startTime
validation present in the create schema; update the Zod schema used by the
update resolver (the object containing startTime:
z.string().datetime().optional(), endTime: z.string().datetime().optional()) to
add a .refine() that enforces when both startTime and endTime are provided that
endTime > startTime (same logic as in the create schema), so that
findOverlappingBooking and row integrity are preserved.
There was a problem hiding this comment.
🧹 Nitpick comments (3)
src/app/debug/bookings/page.tsx (1)
447-451: Hardcoded"15 min"in the debug output won't track changes toPICKUP_WAIT_TIME_MINUTES.The
PICKUP_WAIT_TIME_MINUTESconstant is already available via the import at the top ofbookings.ts(server-side). The client already receivestotalBookingMinutesanddrivingTimeMinutesin the query result; the pickup-wait portion can be derived rather than hardcoded.♻️ Derive pickup wait from API data
- Total booking time: {estimatedEndQuery.data.totalBookingMinutes} min (15 min pickup - wait + {estimatedEndQuery.data.drivingTimeMinutes} min travel) + Total booking time: {estimatedEndQuery.data.totalBookingMinutes} min ( + {estimatedEndQuery.data.totalBookingMinutes - estimatedEndQuery.data.drivingTimeMinutes} min pickup + wait + {estimatedEndQuery.data.drivingTimeMinutes} min travel)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/debug/bookings/page.tsx` around lines 447 - 451, Replace the hardcoded "15 min" in the debug string with a derived pickup-wait value calculated from the query result: compute pickupWait = estimatedEndQuery.data.totalBookingMinutes - estimatedEndQuery.data.drivingTimeMinutes (guard for undefined/null), then render that pickupWait value (e.g., `${pickupWait} min`) instead of the literal "15 min"; update the debug lines that reference estimatedEndQuery.data.totalBookingMinutes and estimatedEndQuery.data.drivingTimeMinutes to use this derived pickupWait so the displayed pickup wait always matches the API data.src/server/api/routers/bookings.ts (2)
409-412:tx as unknown as typeof dbsilences the type mismatch betweendband its transaction object.Drizzle doesn't provide a type for transactions, so you need to extract it yourself. Widening
DbContextto accept both avoids the double cast at both call sites (lines 412 and 588):♻️ Type-safe DbContext
-type DbContext = { db: typeof db }; +type DbTransaction = Parameters<Parameters<typeof db.transaction>[0]>[0]; +type DbContext = { db: typeof db | DbTransaction };Then remove the casts at each call site:
-await validateDriverForSlot( - { db: tx as unknown as typeof db }, +await validateDriverForSlot( + { db: tx }, { ... }, );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/server/api/routers/bookings.ts` around lines 409 - 412, The current code silences a type mismatch by casting the transaction object (tx) with "tx as unknown as typeof db" when calling validateDriverForSlot; instead, update your DbContext type so it accepts both the normal db and a transaction instance (widen the context type or make it generic over the DB/transaction shape), then change validateDriverForSlot (and any other functions that accept DbContext) to use that widened/generic DbContext type so you can pass tx directly from ctx.db.transaction without any casts; locate usages around validateDriverForSlot, ctx.db.transaction, and other callers (the other cast usage referenced in the review) and remove the double-casts once the DbContext type is adjusted.
29-29:travelTimeCachenever evicts expired entries — stale entries accumulate indefinitely.The Map TTL is only checked on reads; expired entries are never removed. In a long-running server process, every unique address-pair queried will remain in memory forever. Consider either:
- Adding a periodic cleanup (e.g.
setInterval) to purge entries whereexpiresAt < Date.now(), or- Replacing the hand-rolled cache with an LRU eviction policy (e.g.
lru-cache).As a secondary note: the cache is process-local, so it provides no benefit on serverless deployments where each cold-start is a fresh module.
♻️ Minimal eviction example
const travelTimeCache = new Map<string, { value: number; expiresAt: number }>(); + +// Periodic cleanup of expired entries to prevent unbounded memory growth +if (typeof setInterval !== "undefined") { + setInterval(() => { + const now = Date.now(); + for (const [key, entry] of travelTimeCache) { + if (entry.expiresAt <= now) travelTimeCache.delete(key); + } + }, TRAVEL_TIME_CACHE_TTL_MS); +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/server/api/routers/bookings.ts` at line 29, The travelTimeCache Map stores entries with expiresAt but never evicts them, causing unbounded memory growth; modify the module to regularly purge expired entries by adding a periodic cleanup (e.g., start a setInterval when the module initializes that iterates travelTimeCache and deletes entries where entry.expiresAt < Date.now()) or swap the Map for an LRU cache implementation (e.g., replace travelTimeCache with an instance from the lru-cache package configured with a max size and TTL) and update any get/set usages in the functions that reference travelTimeCache to use the new cache API; ensure the cleanup interval is cleared on shutdown if using setInterval.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/server/api/routers/bookings.ts`:
- Around line 41-53: assertCanAccessBooking currently only allows admins and
agency owners (role "admin" or agencyId === session.user.id) which blocks
assigned drivers from accessing their own bookings; update the guard and its
call sites (getById, update, cancel) so it also permits a session user whose id
matches the booking.driverId. Concretely, change assertCanAccessBooking to
accept an optional driverId (e.g., assertCanAccessBooking(session, agencyId,
driverId?)) or accept the booking object, then in the body allow access when
session.user.id === driverId; update the calls in getById, update, and cancel to
pass the booking's driverId (or booking) before throwing TRPCError.
- Around line 328-336: The code currently caches FALLBACK_TRAVEL_MINUTES when
getTravelTimeMinutes returns null; change the logic so that when apiResult ===
null you set usedFallback = true and drivingTimeMinutes =
FALLBACK_TRAVEL_MINUTES but do NOT write to travelTimeCache (only write cache
entries when apiResult !== null). Locate the block around getTravelTimeMinutes,
cacheKey, travelTimeCache.set and adjust it so travelTimeCache.set is executed
only if apiResult is a real number (apiResult !== null), leaving
usedCached/usedFallback behavior unchanged.
- Around line 539-541: The update input schema for bookings currently defines
startTime and endTime as z.string().datetime().optional() but lacks the same
.refine() check used in the create schema to enforce endTime > startTime, so
supply of both can produce an inverted range and break findOverlappingBooking;
update the "update" input schema (the update Zod object that contains startTime
and endTime) to add a conditional refine that, when both startTime and endTime
are present, parses them and ensures endTime > startTime (mirroring the create
schema's validation) and returns a proper Zod error if the condition fails.
---
Nitpick comments:
In `@src/app/debug/bookings/page.tsx`:
- Around line 447-451: Replace the hardcoded "15 min" in the debug string with a
derived pickup-wait value calculated from the query result: compute pickupWait =
estimatedEndQuery.data.totalBookingMinutes -
estimatedEndQuery.data.drivingTimeMinutes (guard for undefined/null), then
render that pickupWait value (e.g., `${pickupWait} min`) instead of the literal
"15 min"; update the debug lines that reference
estimatedEndQuery.data.totalBookingMinutes and
estimatedEndQuery.data.drivingTimeMinutes to use this derived pickupWait so the
displayed pickup wait always matches the API data.
In `@src/server/api/routers/bookings.ts`:
- Around line 409-412: The current code silences a type mismatch by casting the
transaction object (tx) with "tx as unknown as typeof db" when calling
validateDriverForSlot; instead, update your DbContext type so it accepts both
the normal db and a transaction instance (widen the context type or make it
generic over the DB/transaction shape), then change validateDriverForSlot (and
any other functions that accept DbContext) to use that widened/generic DbContext
type so you can pass tx directly from ctx.db.transaction without any casts;
locate usages around validateDriverForSlot, ctx.db.transaction, and other
callers (the other cast usage referenced in the review) and remove the
double-casts once the DbContext type is adjusted.
- Line 29: The travelTimeCache Map stores entries with expiresAt but never
evicts them, causing unbounded memory growth; modify the module to regularly
purge expired entries by adding a periodic cleanup (e.g., start a setInterval
when the module initializes that iterates travelTimeCache and deletes entries
where entry.expiresAt < Date.now()) or swap the Map for an LRU cache
implementation (e.g., replace travelTimeCache with an instance from the
lru-cache package configured with a max size and TTL) and update any get/set
usages in the functions that reference travelTimeCache to use the new cache API;
ensure the cleanup interval is cleared on shutdown if using setInterval.
|
merge conflicts @Lujarios |
Summary by CodeRabbit
New Features
Bug Fixes
Chores